Skip to content

Multi-currency: FX conversion and reporting currency - #56

Merged
ItsThompson merged 30 commits into
mainfrom
mc/04-fx-conversion-legacy-reporting
Aug 24, 2026
Merged

ItsThompson merged 30 commits into
mainfrom
mc/04-fx-conversion-legacy-reporting

Conversation

@ItsThompson

@ItsThompson ItsThompson commented Aug 15, 2026 •

Copy link
Copy Markdown
Owner

Summary

Implements FX-backed foreign-currency expense creation, mixed-currency expense suggestions and log, and reporting-currency authority across dashboard/history/health-score. Period reporting currency is the single source of truth for money formatting and computation. FX conversion safe-fails on provider outages: no ledger row is written and the form stays ready for retry. Legacy rows are migrated by mc/03 startup backfill; this PR adds integrity telemetry for rows missing required snapshot fields.

What changed

Backend (Go)

  • Expense FX wiring (fx_client.go, expense.go, config/): Injected FX gRPC client via FX_SERVICE_ADDR. Create-expense validates currencies, calls ConvertAmount when transaction != reporting currency, and writes both money snapshots. FX failures map to CONVERSION_UNAVAILABLE (503) with no ledger write.
  • Expense suggestions (model/suggestions.go, service/suggestions.go, repository/immudb.go): Suggestion models include canonical transaction amount and currency; repository selects and maps transaction_amount/transaction_currency directly.
  • Health score (healthscore_service.go, healthscore.go, healthscore_trend.go): computeHealthScore uses period.ReportingCurrency instead of default settings. FormulaVersion bumped 2 -> 3. Stored scores get backfilled reporting currency. Trend points carry per-period reporting currency.
  • Dashboard and reporting (dashboard.go, model/requests.go): Spending trends and historical comparison carry per-point reporting currency. HistoricalComparison adds PreviousReportingCurrency and Comparable; cross-currency pairs suppress change percent and rolling average.
  • Reporting currency migration (000006_add_period_reporting_currency.*.sql): Three-step backfill (default settings currency, auth user currency, app fallback). Validates every row has a supported currency before adding NOT NULL + CHECK constraint.
  • Snapshot integrity telemetry (repository/immudb.go): Rows missing required snapshot fields return a typed SnapshotIntegrityError and emit expense_snapshot_integrity_error telemetry.

Frontend (TypeScript/React)

  • Expense autocomplete: Suggestion items display transaction currency code. Accepting a suggestion fills transaction amount and currency; foreign-currency suggestions trigger FX conversion on submit.
  • Expense log (expense-table-columns.tsx): Mixed-currency rows show transaction amount and secondary reporting amount labeled "Budget impact". Amount column sorts by reporting amount. Transaction and reporting currency filters are distinct.
  • History: Rows format with row.period.reportingCurrency instead of user.currency. Adjacent rows with different currencies show "Δ not comparable (different currency)".
  • Dashboard widgets: Historical comparison formats previous period in its own currency and guards change percent with comparable. Recent expenses use reportingAmount.
  • Core types: Added reportingCurrency to HealthScore, HealthScoreTrendPoint, and TrendPoint; added previousReportingCurrency and comparable to HistoricalComparison; made transactionAmount, reportingAmount, and reportingCurrency required on Expense.

Tests

All suites pass: expense, finance, fx, and frontend (finance 53 files/509 tests, core 88, api 187). gofmt and go vet clean.

Notes

  • Migration 000006 hardcodes USD as the app fallback currency; a configurable fallback can be a follow-up.
  • SpendingTrendChart uses a single period currency for Y-axis formatting; mixed-currency trend-axis normalization is a candidate follow-up.
  • Formula version bump 2 -> 3 forces lazy recompute of stored health scores (old scores used the default settings currency).

@ItsThompson ItsThompson changed the title mc/04 fx conversion legacy reporting Multi-currency: FX conversion, legacy snapshot migration, and reporting Aug 15, 2026
@ItsThompson
ItsThompson force-pushed the mc/04-fx-conversion-legacy-reporting branch from 228597a to 91a8e19 Compare August 15, 2026 22:49
@ItsThompson
ItsThompson force-pushed the mc/04-fx-conversion-legacy-reporting branch from 91a8e19 to 83ef524 Compare August 15, 2026 22:55
@ItsThompson
ItsThompson force-pushed the mc/04-fx-conversion-legacy-reporting branch from 83ef524 to da072b0 Compare August 15, 2026 23:10
@ItsThompson
ItsThompson force-pushed the mc/04-fx-conversion-legacy-reporting branch from 402e662 to 2d68e9b Compare August 15, 2026 23:53
@ItsThompson
ItsThompson force-pushed the mc/04-fx-conversion-legacy-reporting branch from 2d68e9b to acd9ed1 Compare August 16, 2026 14:52
@ItsThompson
ItsThompson force-pushed the mc/04-fx-conversion-legacy-reporting branch from acd9ed1 to f548caf Compare August 16, 2026 15:17
@ItsThompson
ItsThompson force-pushed the mc/04-fx-conversion-legacy-reporting branch 2 times, most recently from 3e6fa9a to ea2bf5f Compare August 16, 2026 18:45
@ItsThompson
ItsThompson force-pushed the mc/04-fx-conversion-legacy-reporting branch from ea2bf5f to 002148a Compare August 18, 2026 23:36
@ItsThompson
ItsThompson force-pushed the mc/04-fx-conversion-legacy-reporting branch 2 times, most recently from 9bc77ae to 60d4235 Compare August 18, 2026 23:51
@ItsThompson
ItsThompson force-pushed the mc/04-fx-conversion-legacy-reporting branch from 60d4235 to 89e07fb Compare August 19, 2026 06:30
@ItsThompson
ItsThompson force-pushed the mc/04-fx-conversion-legacy-reporting branch 3 times, most recently from dd99103 to 7f1e87f Compare August 20, 2026 14:58
Base automatically changed from mc/03-identity-money-snapshots to main August 20, 2026 15:10
…ency across read paths

Move the service-layer legacy snapshot resolution out of the FX-wiring
commit. Every read path (expense list, detail, correction history,
pro-rata group, and export stream) resolves repository-synthesized
legacy rows to the period reporting currency and emits normalization
telemetry, without failing the read when period context is unavailable.
- gofmt suggestions model/service files
- rename misleading legacy-suggestion test
- name and document the reporting-currency fallback
- label dual amounts as budget impact for accessibility
- scope mobile mixed-currency test to the mobile list
- make canonical suggestion fields optional so fallbacks are meaningful
- add foreign-suggestion-currency integration test
- Collapse mapFxError to a single safe CONVERSION_UNAVAILABLE mapping; the
  dead gRPC-code switch implied distinctions that do not exist.
- Normalize the period reporting currency once in CreateExpense and use it
  for validation, the identity-vs-FX decision, snapshots, and the FX target.
- Require a non-nil FxClient: NewExpenseService panics on nil instead of
  leaking a test-only runtime fallback; tests use a loud stubFxClient.
- Return and close the FX gRPC connection in main.go, matching the finance
  client pattern.
- Drop unused FxConvertResponse echo fields and use the shared exchange
  source constant in tests.
- Assert RequestedAt in FX success tests via a fixed clock seam.
Spec 05 requires Expense to preserve FX error categories rather than
collapse every gRPC status into 503. mapFxError now inspects status.Code:

- Unavailable/FailedPrecondition -> 503 CONVERSION_UNAVAILABLE
- InvalidArgument UNSUPPORTED_CURRENCY -> 400 UNSUPPORTED_CURRENCY
- InvalidArgument INVALID_AMOUNT -> 400 VALIDATION_ERROR
- Internal/unclassified -> 500 INTERNAL_SERVER_ERROR (reported)
- non-gRPC transport failure -> 503 CONVERSION_UNAVAILABLE

Also address review-7 nits: report an unsupported period reporting
currency as a 500 internal invariant violation (not a retryable 503),
rename the misnamed FX-client-unavailable test, and replace the
always-true repo matcher with mock.AnythingOfType.
…g currency first

- Log non-503 FX failures (Internal/unclassified) as Error with event
  foreign_currency_conversion_failed instead of the misleading
  foreign_currency_conversion_unavailable Info event.
- Validate the period reporting currency before resolving the transaction
  currency so a corrupted reporting currency surfaces as a 500 internal
  invariant violation in every branch, including the no-currency-fields
  defaulting path, instead of a 400 transaction-currency error.
- Add tests for both behaviors.
- Remove LegacySynthesized/PartialSnapshotFields and service-layer legacy normalization; rely on InitSchema backfill and version-0 error.
- Read suggestion transaction columns directly without the legacy amount/currency fallback; keep version-1 integrity telemetry.
- Update finance tests for the removed reporting-amount fallback and read the shared currency catalog from code.
- Make transactionAmount, reportingAmount, and reportingCurrency required and read them directly without legacy fallbacks.
- Update finance and shell fixtures/mocks to supply the required snapshot fields.
- Fix shell mock reporting-currency fields for comparison, health score, and trends.
@ItsThompson
ItsThompson force-pushed the mc/04-fx-conversion-legacy-reporting branch from 34e0698 to 5eeafb3 Compare August 20, 2026 16:04
@ItsThompson ItsThompson changed the title Multi-currency: FX conversion, legacy snapshot migration, and reporting Multi-currency: FX conversion and reporting currency Aug 20, 2026
Comment thread frontend/apps/finance/src/features/expense-log/hooks/useExpenseFilters.ts Outdated
Comment thread frontend/apps/finance/src/features/expense-log/hooks/useExpenseLogData.ts Outdated
Comment thread frontend/apps/finance/src/features/history/HistoryFeature.tsx Outdated
Comment thread frontend/apps/finance/src/features/history/types.ts
Comment thread services/expense/internal/repository/immudb.go
Comment thread services/expense/internal/service/expense.go Outdated
Comment thread services/expense/internal/service/suggestions.go Outdated
Comment thread services/finance/internal/model/requests.go
Comment thread services/finance/internal/service/dashboard.go
@ItsThompson
ItsThompson merged commit 45ea181 into main Aug 24, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant